/*
 * @CopyRight:
 * FISCO-BCOS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * FISCO-BCOS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with FISCO-BCOS.  If not, see <http://www.gnu.org/licenses/>
 * (c) 2016-2020 fisco-dev contributors.
 */

/**
 * @brief: ut for WorkingSealerManagerPrecompiled
 * @file : test_WorkingSealerManagerPrecompiled.cpp
 * @author: yujiechen
 * @date: 2020-06-22
 */
#include "test_WorkingSealerManagerPrecompiled.h"

using namespace dev;
using namespace dev::test;

BOOST_FIXTURE_TEST_SUITE(WorkingSealerManagerTest, TestOutputHelperFixture)

// no need to rotate sealers for all the node are working sealers
BOOST_AUTO_TEST_CASE(testRotateWorkingSealerWithoutRotate)
{
    auto fixture = std::make_shared<WorkingSealerManagerFixture>();
    fixture->initWorkingSealerManagerFixture(4, 4);

    // case1: valid vrf generated by some sealer
    LOG(INFO) << LOG_DESC("testRotateWorkingSealerWithoutRotate: test normal case");
    auto vrfInputWithProof = fixture->generateVRFProof(fixture->context, fixture->vrfPrivateKey);
    dev::eth::ContractABI abi;
    auto in = abi.abiIn(WSM_METHOD_ROTATE_STR, fixture->vrfPublicKey, vrfInputWithProof.first,
        vrfInputWithProof.second);

    auto sealerAccount = dev::toAddress(fixture->m_keyPair.pub());
    BOOST_CHECK_NO_THROW(fixture->workingSealerManagerPrecompiled->call(
        fixture->context, bytesConstRef(&in), sealerAccount, sealerAccount));
    BOOST_CHECK(fixture->getSystemConfigByKey(INTERNAL_SYSTEM_KEY_NOTIFY_ROTATE) == "");

    // get the current workingSealers and sealers
    fixture->getSealerList();
    BOOST_CHECK(fixture->m_workingSealerList == fixture->sealerList);
    BOOST_CHECK(fixture->m_pendingSealerList.size() == 0);
    auto workingSealers = fixture->m_workingSealerList;
    auto pendingSealers = fixture->m_pendingSealerList;
    auto orgSealerList = fixture->sealerList;
    // call multiple times for different sealers
    for (auto const& _node : orgSealerList)
    {
        sealerAccount = dev::toAddress(_node);
        BOOST_CHECK_NO_THROW(fixture->workingSealerManagerPrecompiled->call(
            fixture->context, bytesConstRef(&in), sealerAccount, sealerAccount));
        fixture->getSealerList();
        // check the result is equal
        BOOST_CHECK(workingSealers == fixture->m_workingSealerList);
        BOOST_CHECK(pendingSealers == fixture->m_pendingSealerList);
        BOOST_CHECK(fixture->m_pendingSealerList.size() == 0);
        BOOST_CHECK(fixture->getSystemConfigByKey(INTERNAL_SYSTEM_KEY_NOTIFY_ROTATE) == "");
    }

    // case2: valid proof, but the origin is not exist in the workingSealers
    LOG(INFO) << LOG_DESC(
        "testRotateWorkingSealerWithoutRotate: valid proof, but the origin is not exist in the "
        "workingSealer");
    auto nonSealerAccount = dev::toAddress(dev::KeyPair::create().pub());
    BOOST_CHECK_THROW(fixture->workingSealerManagerPrecompiled->call(
                          fixture->context, bytesConstRef(&in), nonSealerAccount, nonSealerAccount),
        PrecompiledException);
    // check INTERNAL_SYSTEM_KEY_NOTIFY_ROTATE flag
    BOOST_CHECK(fixture->getSystemConfigByKey(INTERNAL_SYSTEM_KEY_NOTIFY_ROTATE) == "1");

    // case3: invalid input(must be lastest hash)
    LOG(INFO) << LOG_DESC("testRotateWorkingSealerWithoutRotate: valid proof, invalid input");
    in = abi.abiIn(WSM_METHOD_ROTATE_STR, fixture->vrfPublicKey, toHex(dev::keccak256("test")),
        vrfInputWithProof.second);
    BOOST_CHECK_THROW(fixture->workingSealerManagerPrecompiled->call(
                          fixture->context, bytesConstRef(&in), nonSealerAccount, nonSealerAccount),
        PrecompiledException);
    BOOST_CHECK(fixture->getSystemConfigByKey(INTERNAL_SYSTEM_KEY_NOTIFY_ROTATE) == "1");

    // case4: invalid public key(the origin is not one of the sealers)
    LOG(INFO) << LOG_DESC("testRotateWorkingSealerWithoutRotate: invalid public key");
    in = abi.abiIn(WSM_METHOD_ROTATE_STR, toHex(bytesConstRef{fixture->m_keyPair.pub().data(), 64}),
        vrfInputWithProof.first, vrfInputWithProof.second);
    BOOST_CHECK_THROW(fixture->workingSealerManagerPrecompiled->call(
                          fixture->context, bytesConstRef(&in), nonSealerAccount, nonSealerAccount),
        PrecompiledException);
    BOOST_CHECK(fixture->getSystemConfigByKey(INTERNAL_SYSTEM_KEY_NOTIFY_ROTATE) == "1");

    // case5: invalid proof
    LOG(INFO) << LOG_DESC("testRotateWorkingSealerWithoutRotate: invalid proof");
    auto fakedKeyPair = dev::KeyPair::create();
    auto fakedVRFKeyPair = fixture->generateVRFKeyPair(fakedKeyPair);
    auto fakedVRFInputWithProof =
        fixture->generateVRFProof(fixture->context, fakedVRFKeyPair.first);
    in = abi.abiIn(WSM_METHOD_ROTATE_STR, fixture->vrfPublicKey, fakedVRFInputWithProof.first,
        fakedVRFInputWithProof.second);
    BOOST_CHECK_THROW(fixture->workingSealerManagerPrecompiled->call(
                          fixture->context, bytesConstRef(&in), sealerAccount, sealerAccount),
        PrecompiledException);
    BOOST_CHECK(fixture->getSystemConfigByKey(INTERNAL_SYSTEM_KEY_NOTIFY_ROTATE) == "1");

    // case6: vaild proof
    LOG(INFO) << LOG_DESC(
        "testRotateWorkingSealerWithoutRotate: rotate success after invalid cases");
    auto blockInfo = fixture->context->blockInfo();
    blockInfo.number += 1;
    fixture->context->setBlockInfo(blockInfo);
    vrfInputWithProof = fixture->generateVRFProof(fixture->context, fixture->vrfPrivateKey);
    in = abi.abiIn(WSM_METHOD_ROTATE_STR, fixture->vrfPublicKey, vrfInputWithProof.first,
        vrfInputWithProof.second);
    sealerAccount = dev::toAddress(fixture->m_keyPair.pub());
    BOOST_CHECK_NO_THROW(fixture->workingSealerManagerPrecompiled->call(
        fixture->context, bytesConstRef(&in), sealerAccount, sealerAccount));
    BOOST_CHECK(fixture->getSystemConfigByKey(INTERNAL_SYSTEM_KEY_NOTIFY_ROTATE) == "0");
}

BOOST_AUTO_TEST_CASE(testRotateOneSealer)
{
    auto fixture = std::make_shared<WorkingSealerManagerFixture>();
    fixture->initWorkingSealerManagerFixture(10, 4, 1);
    fixture->getSealerList();
    auto orgWorkingSealers = fixture->m_workingSealerList;
    auto orgPendingSealers = fixture->m_pendingSealerList;
    // generate proof
    auto workingSealer = orgWorkingSealers[0];
    auto workingSealerVRFKeyPair =
        fixture->generateVRFKeyPair(fixture->m_publicKey2KeyPair[workingSealer]);
    auto proof = fixture->generateVRFProof(fixture->context, workingSealerVRFKeyPair.first);

    dev::eth::ContractABI abi;
    auto in =
        abi.abiIn(WSM_METHOD_ROTATE_STR, workingSealerVRFKeyPair.second, proof.first, proof.second);
    auto origin = dev::toAddress(fixture->m_publicKey2KeyPair[workingSealer].pub());
    BOOST_CHECK_NO_THROW(fixture->workingSealerManagerPrecompiled->call(
        fixture->context, bytesConstRef(&in), origin, origin));
    fixture->getSealerList();
    auto currentWorkingSealerList = fixture->m_workingSealerList;
    auto currentPendingSealerList = fixture->m_pendingSealerList;
    BOOST_CHECK(fixture->m_workingSealerList.size() == 4);
    BOOST_CHECK(fixture->m_pendingSealerList.size() == 6);
    BOOST_CHECK(fixture->m_workingSealerList != orgWorkingSealers);
    BOOST_CHECK(fixture->m_pendingSealerList != orgPendingSealers);

    // check the result is consistent for every sealer
    for (size_t i = 0; i < fixture->sealerList.size(); i++)
    {
        auto otherNodeFixture = std::make_shared<WorkingSealerManagerFixture>(fixture, 4, 1);
        BOOST_CHECK_NO_THROW(otherNodeFixture->workingSealerManagerPrecompiled->call(
            otherNodeFixture->context, bytesConstRef(&in), origin, origin));
        otherNodeFixture->getSealerList();
        BOOST_CHECK(otherNodeFixture->m_workingSealerList.size() == 4);
        BOOST_CHECK(otherNodeFixture->m_pendingSealerList.size() == 6);
        BOOST_CHECK(currentWorkingSealerList == otherNodeFixture->m_workingSealerList);
        BOOST_CHECK(currentPendingSealerList == otherNodeFixture->m_pendingSealerList);
    }
}


void updateProof(WorkingSealerManagerFixture::Ptr fixture,
    std::pair<std::string, std::string>& workingSealerKeyPair,
    std::pair<std::string, std::string>& proof, bytes& in, Address& origin)
{
    fixture->getSealerList();
    dev::h512 sealer;
    if (fixture->m_workingSealerList.size() > 0)
    {
        LOG(INFO) << LOG_DESC("updateProof")
                  << LOG_KV("workingSealerSize", fixture->m_workingSealerList.size());
        sealer = fixture->m_workingSealerList[0];
    }
    else
    {
        sealer = fixture->m_pendingSealerList[0];
        LOG(INFO) << LOG_DESC("updateProof with pendingSealer for workingSealer size is 0");
    }
    workingSealerKeyPair = fixture->generateVRFKeyPair(fixture->m_publicKey2KeyPair[sealer]);
    proof = fixture->generateVRFProof(fixture->context, workingSealerKeyPair.first);
    dev::eth::ContractABI abi;
    in = abi.abiIn(WSM_METHOD_ROTATE_STR, workingSealerKeyPair.second, proof.first, proof.second);
    origin = dev::toAddress(fixture->m_publicKey2KeyPair[sealer].pub());
}

BOOST_AUTO_TEST_CASE(testRotateMultiSealers)
{
    auto fixture = std::make_shared<WorkingSealerManagerFixture>();
    // init with 10 sealers and 6 working sealers
    fixture->initWorkingSealerManagerFixture(10, 6, 1);

    // remove 2 workingSealers into observer
    fixture->getSealerList();
    fixture->updateNodeListType(fixture->m_workingSealerList, 2, NODE_TYPE_OBSERVER);

    // get the current working sealers
    std::pair<std::string, std::string> workingSealerKeyPair;
    std::pair<std::string, std::string> proof;
    bytes in;
    Address origin;
    updateProof(fixture, workingSealerKeyPair, proof, in, origin);
    BOOST_CHECK_NO_THROW(fixture->workingSealerManagerPrecompiled->call(
        fixture->context, bytesConstRef(&in), origin, origin));
    fixture->getSealerList();

    BOOST_CHECK(fixture->m_workingSealerList.size() == 6);
    BOOST_CHECK(fixture->m_pendingSealerList.size() == 2);

    // remove 1 pendingSealer, remove 2 workingSealers
    fixture->updateNodeListType(fixture->m_pendingSealerList, 1, NODE_TYPE_OBSERVER);
    fixture->updateNodeListType(fixture->m_workingSealerList, 2, NODE_TYPE_OBSERVER);
    updateProof(fixture, workingSealerKeyPair, proof, in, origin);

    auto blockInfo = fixture->context->blockInfo();
    blockInfo.number += 1;
    fixture->context->setBlockInfo(blockInfo);
    BOOST_CHECK_NO_THROW(fixture->workingSealerManagerPrecompiled->call(
        fixture->context, bytesConstRef(&in), origin, origin));
    fixture->getSealerList();
    BOOST_CHECK(fixture->m_workingSealerList.size() == 5);
    BOOST_CHECK(fixture->m_pendingSealerList.size() == 0);

    // add 1 pending sealer, remove 2 workingSealers
    fixture->updateNodeListType(fixture->m_observerList, 1, NODE_TYPE_SEALER);
    fixture->updateNodeListType(fixture->m_workingSealerList, 2, NODE_TYPE_OBSERVER);
    updateProof(fixture, workingSealerKeyPair, proof, in, origin);

    blockInfo = fixture->context->blockInfo();
    blockInfo.number += 1;
    fixture->context->setBlockInfo(blockInfo);
    BOOST_CHECK_NO_THROW(fixture->workingSealerManagerPrecompiled->call(
        fixture->context, bytesConstRef(&in), origin, origin));
    fixture->getSealerList();
    BOOST_CHECK(fixture->m_workingSealerList.size() == 4);
    BOOST_CHECK(fixture->m_pendingSealerList.size() == 0);

    // remove all the workingSealers into sealer
    auto workingSealerSize = fixture->m_workingSealerList.size();
    fixture->updateNodeListType(fixture->m_workingSealerList, 3, NODE_TYPE_SEALER);
    fixture->updateNodeListType(
        fixture->m_workingSealerList, workingSealerSize - 3, NODE_TYPE_OBSERVER);
    updateProof(fixture, workingSealerKeyPair, proof, in, origin);

    blockInfo = fixture->context->blockInfo();
    blockInfo.number += 1;
    fixture->context->setBlockInfo(blockInfo);
    BOOST_CHECK_NO_THROW(fixture->workingSealerManagerPrecompiled->call(
        fixture->context, bytesConstRef(&in), origin, origin));
    fixture->getSealerList();
    BOOST_CHECK(fixture->m_workingSealerList.size() == 3);
    BOOST_CHECK(fixture->m_pendingSealerList.size() == 0);
}

BOOST_AUTO_TEST_SUITE_END()